<?php
namespace App\Security;
use App\Controller\Services\CaptchaController;
use App\Entity\Account;
use App\Entity\EgeeClient;
use Psr\Log\LoggerInterface;
use Doctrine\ORM\EntityManagerInterface;
use App\Domain\Encryption\HashingService;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Csrf\CsrfToken;
use App\Exception\RecaptchaVerificationException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
class CustomerAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'login';
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $hashingService;
private $logger;
public function __construct(
EntityManagerInterface $entityManager,
UrlGeneratorInterface $urlGenerator,
CsrfTokenManagerInterface $csrfTokenManager,
HashingService $hashingService,
LoggerInterface $logger
) {
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->hashingService = $hashingService;
$this->logger = $logger;
}
public function supports(Request $request)
{
return self::LOGIN_ROUTE === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$ip = $_SERVER['REMOTE_ADDR'];
$dateDuJour = date("Ymd");
$varPath = getcwd().'/../var';
$chemin = $varPath.'/captcha';
if (!is_dir($chemin)) {
// Crée le dossier avec les permissions 0755, et true pour créer les dossiers parents si nécessaire
if (!mkdir($chemin, 0755, true)) {
die('Échec lors de la création du dossier...');
}
}
$fail_log = $chemin . '/'.$dateDuJour.'.log';
$max_attempts = 5;
$time_window = 15 * 60; // 15 minutes
// ❗ Vérifie si l’IP est déjà bloquée
if ($this->verifierSiIpBloquee($ip, $fail_log, $max_attempts, $time_window)) {
$this->logger->info('reCAPTCHA rejeté', ['IP bloquée']);
throw new RecaptchaVerificationException();
//die("Trop de tentatives échouées. Réessayez plus tard.");
}
/*
* Clé Google Recaptcha v3
* DEV :
* - clé secrète 6LcnjyoaAAAAAPhi6K_AxoW47WPWJHNQCDEgTtMS
* - clé site 6LcnjyoaAAAAANHwcvnepkwrR3Xby2e7FTPTTG_r
*
* PROD :
* - clé secrète 6LcjorMZAAAAAPZ2jHNngd9MpmsUcO9pv6oNB3yx
* - clé site 6LcjorMZAAAAAEKYV5kfyGo_K-oBN_dZGXLAs4N3
*/
//if (null !== $request->request->get('recaptcha_response')) {
if (isset($_POST['formulaire_jeton']) && $_POST['formulaire_jeton'] == $_SESSION['captcha_token']) {
if (time() - $_SESSION['formulaire_time'] < 3) {
$this->logger->info('reCAPTCHA rejeté', ['Soumission trop rapide : bot probable']);
throw new RecaptchaVerificationException();
}
$trap_field = $_SESSION['formulaire_piege'] ?? '';
if (!empty($_POST[$trap_field])) {
$this->logger->info('reCAPTCHA rejeté', ['Bot détecté (champ dynamique)']);
throw new RecaptchaVerificationException();
}
//$recaptcha_url = 'https://www.google.com/recaptcha/api/siteverify';
/*$host = $request->getHost();
switch ($host) {
case 'ec-eau-2024.iti-communication.net':
$recaptcha_secret = '6LcnjyoaAAAAAPhi6K_AxoW47WPWJHNQCDEgTtMS';
break;
default:
$recaptcha_secret = '6LcjorMZAAAAAPZ2jHNngd9MpmsUcO9pv6oNB3yx';
}
$recaptcha_response = $request->request->get('recaptcha_response');
$params = [
'secret' => $recaptcha_secret,
'response' => $recaptcha_response,
];*/
// Requete de vérification du score
/*$ch = curl_init($recaptcha_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 2); // timeout max 2s
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
$start = microtime(true);
$response = curl_exec($ch);
$duration = microtime(true) - $start;
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
$this->logger->error('reCAPTCHA API error: ' . $error);
throw new RecaptchaVerificationException();
}
curl_close($ch);
$recaptcha = json_decode($response);
if ($duration > 1.5) {
$this->logger->warning('reCAPTCHA API slow response', ['duration' => $duration]);
}*/
//if (isset($recaptcha->score) && $recaptcha->score >= 0.5) {
// $this->logger->info('reCAPTCHA accepté', ['score' => $recaptcha->score ?? 'null']);
$this->logger->info('reCAPTCHA accepté', ['OK']);
$credentials = [
'reference' => $request->request->get('reference'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['reference']
);
return $credentials;
/*} else {
$this->logger->info('reCAPTCHA rejeté', ['score' => $recaptcha->score ?? 'null']);
throw new RecaptchaVerificationException();
}*/
} else {
$this->loguerEchecs($ip, $fail_log);
$this->logger->info('reCAPTCHA rejeté', ['Captcha incorrect']);
//throw new RecaptchaVerificationException();
throw new InvalidCsrfTokenException('reCAPTCHA manquant.');
}
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$account = $this->entityManager->getRepository(Account::class)->getAccountByReference(
$credentials['reference']
);
$client = $this->entityManager->getRepository(EgeeClient::class)->findOneBy(
['cliReference' => $credentials['reference']]
);
if (!$account) { // Si usager non présent dans la table account on cherche si présent dans l'ancienne plareforme
$varPath = getcwd().'/../var';
$accountsLegacy = [];
if (file_exists($varPath.'/account')) {
$fileAccount = fopen($varPath.'/account', 'r');
while (!feof($fileAccount)) {
$line = fgets($fileAccount);
$line = explode(',', $line);
if (isset($line[0]) && isset($line[1]) && isset($line[2])) {
$accountsLegacy[$line[0]] = [
'password' => $line[1],
'email' => $line[2],
];
}
}
fclose($fileAccount);
}
if (isset($accountsLegacy[$credentials['reference']]) && isset($client)) { // Si present dans le fichier account de rétrocompatibilité
$accountEmail = preg_replace("/\r|\n/", '', $accountsLegacy[$credentials['reference']]['email']);
if ('' == $accountEmail && $client->getCliMel()) {
$accountEmail = $client->getCliMel();
}
if (crypt(
$credentials['password'],
$accountsLegacy[$credentials['reference']]['password']
) === $accountsLegacy[$credentials['reference']]['password']) {
// Si authentification rétrocompatible réussie on créer une entrée dans la table account
$this->createAccountRetroCompatibility(
$credentials['reference'],
$accountEmail,
$credentials['password']
);
// On supprime l'entree dans le fichier
unset($accountsLegacy[$credentials['reference']]);
$fileAccount = fopen($varPath.'/account', 'w');
foreach ($accountsLegacy as $key => $line) {
fwrite($fileAccount, $key.','.$line['password'].','.$line['email']);
}
fclose($fileAccount);
$account = $this->entityManager->getRepository(Account::class)->getAccountByReference(
$credentials['reference']
);
$this->logger->info(
'Connexion de l\'utilisateur - Création de l\'usager (Retrocompatibilité du mot de passe)',
['customer' => $credentials['reference']]
);
} else {
// Si l'authentification via la rétrocompatibilité à échouée on créer une entrée dans la table account
$this->createAccountRetroCompatibility(
$credentials['reference'],
$accountEmail,
'qU39npeP4mVaB655atRS4J6S'
);
$this->logger->error(
'Connexion de l\'utilisateur - Echec authentification rétrocompatible, création user verrouillé',
['customer' => $credentials['reference']]
);
throw new CustomUserMessageAuthenticationException('La référence client est introuvable, en cas de problème veuillez utiliser le lien "mot de passe oublié"');
}
} elseif ($client) { // Si usager non présent dans la table account, non présent dans le fichier account mais present dans la table client
if ($client->getCliMel()) {
$accountEmail = $client->getCliMel();
} else {
$accountEmail = '';
}
if ($client->getCliTelephone()) {
$accountTel = $client->getCliTelephone();
} else {
$accountTel = '';
}
if (crypt(
$credentials['password'],
$client->getCliMdp()
) === $client->getCliMdp()) {
$this->createAccountRetroCompatibility(
$credentials['reference'],
$accountEmail,
$credentials['password'],
$accountTel
);
$account = $this->entityManager->getRepository(Account::class)->getAccountByReference(
$credentials['reference']
);
$this->logger->info(
'Connexion de l\'utilisateur - Création de l\'usager (Account non présent)',
['customer' => $credentials['reference']]
);
} else {
// Si l'authentification via la récupération des infos de la table client à échouée on créer une entrée dans la table account
$this->createAccountRetroCompatibility(
$credentials['reference'],
$accountEmail,
'qU39npeP4mVaB655atRS4J6S',
$accountTel
);
$this->logger->error(
'Connexion de l\'utilisateur - Echec authentification via récupération des infos table client, création user verrouillé',
['customer' => $credentials['reference']]
);
throw new CustomUserMessageAuthenticationException('La référence client est introuvable, en cas de problème veuillez utiliser le lien "mot de passe oublié"');
}
} else { // usager inconnu (non présent dans le fichier account ni dans la table client) ou authentification incorrecte
$this->logger->error(
'Connexion de l\'utilisateur - Echec référence introuvable',
['customer' => $credentials['reference']]
);
throw new CustomUserMessageAuthenticationException('La référence client est introuvable');
}
}
return $account;
}
private function createAccountRetroCompatibility($reference, $email, $password, $telephone = '')
{
$account = new Account();
$account->setReference($reference);
$account->setEmail($email);
if ($telephone) {
$account->setPhoneNumber($telephone);
}
$account->setPassword($this->hashingService->hashPassword($password));
$account->setIsSetupNeeded(1);
$account->setUpdatedAt(new \DateTime());
$this->entityManager->persist($account);
$this->entityManager->flush();
}
public function checkCredentials($credentials, UserInterface $account)
{
return password_verify($credentials['password'], $account->getPassword());
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$account = $this->entityManager->getRepository(Account::class)->find($token->getUser()->getAutoId());
$varPath = getcwd().'/../var';
$reference = preg_replace('/\s+/', '', $account->getReference());
$dossierParent = substr($reference, 0, 3);
$filename = $varPath.'/accounts/'.$dossierParent.'/'.$reference;
if (!file_exists($varPath.'/accounts')) {
mkdir($varPath.'/accounts', 0777, true);
}
if (!file_exists($varPath.'/accounts/'.$dossierParent)) {
mkdir($varPath.'/accounts/'.$dossierParent, 0777, true);
}
if (!file_exists($filename)) {
$timestamp = time();
$result = file_put_contents($filename, $timestamp);
if (false !== $result) {
$this->logger->info('Creation fichier timestamp', ['customer' => $account->getReference()]);
} else {
$this->logger->info('Impossible d\'écrire dans le fichier timestamp', ['customer' => $account->getReference()]);
}
}
// --------------------------------------------------
// On ajoute une vérification pour forcer le
// changement de mot de passe au bout de 150 jours
/*$derniereMaj = file_get_contents($filename);
$delaiMaj = $derniereMaj + (86400 * 150);
if($delaiMaj < time()) {
$account->setIsSetupNeeded(1);
$this->entityManager->flush();
}*/
// --------------------------------------------------
if ($account->getIsSetupNeeded()) {
$this->logger->info(
'Connexion de l\'utilisateur - Réussite première configuration',
['customer' => $account->getReference()]
);
return new RedirectResponse($this->urlGenerator->generate('account-setup'));
}
$this->logger->info('Connexion de l\'utilisateur - Réussite', ['customer' => $account->getReference()]);
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('dashboard'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
private function hashLegacyPassword($password, $cost)
{
$salt = substr(base64_encode(openssl_random_pseudo_bytes(17)), 0, 22);
$salt = str_replace('+', '.', $salt);
$param = '$'.implode('$', ['2y', str_pad($cost, 2, '0', STR_PAD_LEFT), $salt]);
return crypt($password, $param);
}
private function verifierSiIpBloquee($ip, $fail_log, $max_attempts, $time_window) {
if (!file_exists($fail_log)) return false;
$lines = file($fail_log, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$now = time();
$recent_fails = 0;
foreach ($lines as $line) {
list($logged_ip, $timestamp) = explode('|', $line);
if ($logged_ip === $ip && ($now - (int)$timestamp) < $time_window) {
$recent_fails++;
}
}
return $recent_fails >= $max_attempts;
}
private function loguerEchecs($ip, $fail_log) {
file_put_contents($fail_log, "$ip|" . time() . "\n", FILE_APPEND);
}
}